/*
 * Copyright (c) 2024-2025.  little3201.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *       https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.leafage.exploiter.service.impl;

import freemarker.template.*;
import io.leafage.exploiter.constants.FieldConstant;
import io.leafage.exploiter.constants.FieldTypeEnum;
import io.leafage.exploiter.domain.Field;
import io.leafage.exploiter.domain.Schema;
import io.leafage.exploiter.dto.SchemaDTO;
import io.leafage.exploiter.repository.ConnectionRepository;
import io.leafage.exploiter.repository.FieldRepository;
import io.leafage.exploiter.repository.MasterPlateRepository;
import io.leafage.exploiter.repository.SchemaRepository;
import io.leafage.exploiter.service.DBService;
import io.leafage.exploiter.service.SchemaService;
import io.leafage.exploiter.vo.ColumnVO;
import io.leafage.exploiter.vo.RenderedMasterPlateVO;
import io.leafage.exploiter.vo.SchemaVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Service
public class SchemaServiceImpl implements SchemaService {

    private static final Logger logger = LoggerFactory.getLogger(SchemaServiceImpl.class);

    private final Configuration cfg = new Configuration(Configuration.VERSION_2_3_33);

    private final ConnectionRepository ConnectionRepository;
    private final SchemaRepository schemaRepository;
    private final FieldRepository fieldRepository;
    private final MasterPlateRepository masterPlateRepository;

    private final DBService dbService;

    /**
     * 转驼峰
     */
    class CamelTemplateMethodEx implements TemplateMethodModelEx {

        @Override
        public Object exec(List arguments) {
            return camel(arguments.get(0).toString());
        }
    }

    /**
     * 转单数
     */
    class SingularTemplateMethodEx implements TemplateMethodModelEx {

        @Override
        public Object exec(List arguments) {
            return toSingular(arguments.get(0).toString());
        }
    }

    /**
     * 转驼峰，并将最后一个转单数
     */
    class CamelAndSingularTemplateMethodEx implements TemplateMethodModelEx {

        @Override
        public Object exec(List arguments) {
            return camelAndSingular(arguments.get(0).toString());
        }
    }

    public SchemaServiceImpl(ConnectionRepository ConnectionRepository, SchemaRepository schemaRepository, FieldRepository fieldRepository,
                             MasterPlateRepository masterPlateRepository, DBService dbService) {
        this.ConnectionRepository = ConnectionRepository;
        this.schemaRepository = schemaRepository;
        this.fieldRepository = fieldRepository;
        this.masterPlateRepository = masterPlateRepository;
        this.dbService = dbService;

        cfg.setDefaultEncoding("UTF-8");
        cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
        // 注册自定义函数
        cfg.setSharedVariable("camel", new CamelTemplateMethodEx());
        cfg.setSharedVariable("singular", new SingularTemplateMethodEx());
        cfg.setSharedVariable("camel_singular", new CamelAndSingularTemplateMethodEx());
    }

    @Override
    public Page<SchemaVO> retrieve(int page, int size, Long connectionId) {
        Pageable pageable = PageRequest.of(page, size);

        return schemaRepository.findAllByConnectionId(connectionId, pageable)
                .map(schema -> convertToVO(schema, SchemaVO.class));
    }

    @Override
    public List<SchemaVO> retrieve(List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return schemaRepository.findAll().stream()
                    .map(schema -> convertToVO(schema, SchemaVO.class)).toList();
        } else {
            return schemaRepository.findAllById(ids).stream()
                    .map(schema -> convertToVO(schema, SchemaVO.class)).toList();
        }
    }

    @Override
    public SchemaVO fetch(Long id) {
        Assert.notNull(id, "id must not be null.");

        return schemaRepository.findById(id)
                .map(schema -> convertToVO(schema, SchemaVO.class)).orElse(null);
    }

    @Override
    public boolean exists(String name, Long id) {
        Assert.hasText(name, "name must not be null.");
        if (id == null) {
            return schemaRepository.existsByName(name);
        }
        return schemaRepository.existsByNameAndIdNot(name, id);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public SchemaVO create(SchemaDTO dto) {
        Schema schema = convertToDomain(dto, Schema.class);
        schema.setEnabled(false);
        schemaRepository.save(schema);

        List<Field> fields = ConnectionRepository.findById(dto.getLinkId()).map(connection ->
                dbService.findAllColumnsByTableName(connection.getHost(),
                                connection.getPort(),
                                connection.getName(),
                                connection.getUsername(),
                                connection.getPassword(),
                                dto.getName())
                        .stream()
                        .filter(columnVO -> !FieldConstant.metadata.contains(columnVO.getColumnName()))
                        .map(columnVO -> {
                            Field field = new Field();
                            field.setSchemaId(schema.getId());
                            constructField(field, columnVO);
                            return field;
                        }).toList()).orElse(Collections.emptyList());

        fieldRepository.saveAll(fields);
        return convertToVO(schema, SchemaVO.class);
    }

    @Override
    public List<SchemaVO> createAll(Iterable<SchemaDTO> iterable) {
        List<Schema> schemas = StreamSupport.stream(iterable.spliterator(), false)
                .map(dto -> convertToDomain(dto, Schema.class)).toList();
        return schemaRepository.saveAll(schemas).stream()
                .map(schema -> convertToVO(schema, SchemaVO.class)).toList();
    }

    @Override
    public SchemaVO modify(Long id, SchemaDTO dto) {
        Assert.notNull(id, "id must not be null.");

        return schemaRepository.findById(id).map(existing -> {
            Schema schema = convert(dto, existing);
            schema = schemaRepository.save(schema);
            return convertToVO(schema, SchemaVO.class);
        }).orElseThrow();
    }

    @Override
    public void sync(Long id) {
        schemaRepository.findById(id).ifPresent(schema -> {
            Map<String, Field> existingFields = fieldRepository.findAllBySchemaId(id).stream()
                    .collect(Collectors.toMap(Field::getName, field -> field));

            List<Field> fields = ConnectionRepository.findById(schema.getConnectionId())
                    .map(connection -> dbService.findAllColumnsByTableName(connection.getHost(),
                                    connection.getPort(),
                                    connection.getName(),
                                    connection.getUsername(),
                                    connection.getPassword(),
                                    schema.getName())
                            .stream()
                            .filter(columnVO -> !FieldConstant.metadata.contains(columnVO.getColumnName()))
                            .map(columnVO -> {
                                Field field = existingFields.get(columnVO.getColumnName());
                                if (field == null) {
                                    field = new Field();
                                    field.setSchemaId(id);
                                }
                                return constructField(field, columnVO);
                            }).toList()).orElse(Collections.emptyList());

            fieldRepository.saveAll(fields);
        });
    }

    @Override
    public boolean enable(Long id) {
        Assert.notNull(id, "id must not be null.");

        Schema schema = schemaRepository.findById(id).orElseThrow();
        if (schema.isEnabled()) {
            return schema.isEnabled();
        }
        return schemaRepository.updateEnabledById(id) > 0;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void remove(Long id) {
        Assert.notNull(id, "id must not be null.");

        List<Long> ids = fieldRepository.findAllBySchemaId(id).stream().map(Field::getId).toList();
        fieldRepository.deleteAllById(ids);

        schemaRepository.deleteById(id);
    }

    @Override
    public void generate(Long id, ZipOutputStream zos) {
        Schema schema = schemaRepository.findById(id).orElseThrow();
        // 准备数据模型
        Map<String, Object> dataModel = createDataModel(schema);

        Path tempDir = Paths.get(System.getProperty("user.dir"), "temp", schema.getName());
        schema.getTemplates().stream().flatMap(templateId -> masterPlateRepository.findById(templateId).stream())
                .forEach(template -> {
                    String formattedName = formatName(schema.getName(), template.getName(), template.getSuffix());
                    Path tempFile = tempDir.resolve(formattedName);
                    writeTemplate(formattedName, template.getContent(), dataModel, tempFile.toString());
                    try {
                        zos.putNextEntry(new ZipEntry(formattedName));
                        Files.copy(tempFile, zos);
                        zos.closeEntry();
                    } catch (IOException e) {
                        logger.error("Failed to process template: {}", template.getSuffix(), e);
                    }
                });
        clearHistory(tempDir);
    }

    @Override
    public List<RenderedMasterPlateVO> preview(Long id) {
        Schema schema = schemaRepository.findById(id).orElseThrow();
        // 准备数据模型
        Map<String, Object> dataModel = createDataModel(schema);

        return schema.getTemplates().stream()
                .flatMap(templateId -> masterPlateRepository.findById(templateId).stream())
                .map(template -> {
                    String formattedName = formatName(schema.getName(), template.getName(), template.getSuffix());
                    RenderedMasterPlateVO templateVO = new RenderedMasterPlateVO();
                    templateVO.setName(formattedName);
                    templateVO.setSuffix(template.getSuffix());
                    templateVO.setType(template.getType());
                    templateVO.setContent(renderTemplate(formattedName, template.getContent(), dataModel));
                    return templateVO;
                })
                .collect(Collectors.collectingAndThen(Collectors.groupingBy(RenderedMasterPlateVO::getType),
                        groupedTemplates -> groupedTemplates.entrySet().stream().map(entry -> {
                            RenderedMasterPlateVO vo = new RenderedMasterPlateVO();
                            vo.setName(entry.getKey());
                            vo.setChildren(entry.getValue());
                            return vo;
                        }).toList()
                ));
    }

    // 创建数据模型
    private Map<String, Object> createDataModel(Schema schema) {
        Map<String, Object> dataModel = new HashMap<>();
        dataModel.put("name", schema.getName());
        dataModel.put("packagePath", schema.getPackagePath());
        List<Field> fields = fieldRepository.findAllBySchemaId(schema.getId());
        dataModel.put("fields", fields);
        return dataModel;
    }

    // 构造
    private Field constructField(Field field, ColumnVO columnVO) {
        field.setName(columnVO.getColumnName());
        field.setDataType(columnVO.getDataType());
        field.setLength(columnVO.getMaximumLength());
        FieldTypeEnum fieldTypeEnum = FieldTypeEnum.getByDbType(columnVO.getDataType());
        if (fieldTypeEnum != null) {
            field.setFieldType(fieldTypeEnum.getJavaType());
            field.setFormType(fieldTypeEnum.getFormType());
            field.setTsType(fieldTypeEnum.getTsType());
        }
        field.setNullable(columnVO.isNullable());
        field.setUnique(columnVO.isUnique());
        return field;
    }

    // 清除历史
    private void clearHistory(Path path) {
        if (!Files.exists(path)) {
            return;
        }
        try (Stream<Path> pathStream = Files.walk(path)) {
            pathStream.sorted(Comparator.reverseOrder()).forEach(file -> {
                try {
                    Files.delete(file);
                } catch (IOException e) {
                    logger.error("File: {} delete failure.", file, e);
                }
            });
        } catch (IOException e) {
            logger.error("File: {} delete failure.", path, e);
        }
    }

    // 渲染模板并将结果写入文件
    private void writeTemplate(String name, String content, Map<String, Object> dataModel, String reference) {
        Path outputPath = Paths.get(reference);

        try {
            // 确保父目录存在
            Files.createDirectories(outputPath.getParent());

            // 使用NIO API进行文件写入
            try (BufferedWriter writer = Files.newBufferedWriter(outputPath, StandardCharsets.UTF_8)) {
                Template freemarkerTemplate = new Template(
                        name,
                        new StringReader(content),
                        cfg);

                freemarkerTemplate.process(dataModel, writer);
            }
        } catch (IOException e) {
            logger.error("Failed to create directories or write file: {}", reference, e);
        } catch (TemplateException e) {
            logger.error("Template rendering failed for template: {}", name, e);
        }
    }

    private String formatName(String schemaName, String templateName, String suffix) {
        if (!templateName.startsWith("%s")) {
            return templateName + suffix;
        }

        String formattedSchemaName = ".ts".equals(suffix)
                ? schemaName.replace("_", "-")
                : camelAndSingular(schemaName);

        return String.format(templateName + suffix, formattedSchemaName);
    }

    // 渲染模板并将结果写入文件
    private String renderTemplate(String name, String content, Map<String, Object> dataModel) {
        // 将模板内容加载到 FreeMarker 并渲染
        try (StringWriter stringWriter = new StringWriter()) {
            Template freemarker = new Template(name, new StringReader(content), cfg);
            freemarker.process(dataModel, stringWriter);
            return stringWriter.toString();
        } catch (IOException | TemplateException e) {
            logger.error("模板渲染异常：{}", e.getMessage(), e);
            return null;
        }
    }

    private String camel(String name) {
        if (!StringUtils.hasText(name)) {
            return name;
        }

        String[] words = name.split("_");
        // 最后一个单词复数转单数
        StringBuilder stringBuilder = new StringBuilder();
        for (String word : words) {
            stringBuilder.append(word.substring(0, 1).toUpperCase()).append(word.substring(1));
        }
        return stringBuilder.toString();
    }

    private String camelAndSingular(String name) {
        if (!StringUtils.hasText(name)) {
            return name;
        }

        String[] words = name.split("_");
        // 最后一个单词复数转单数
        words[words.length - 1] = toSingular(words[words.length - 1]);
        StringBuilder stringBuilder = new StringBuilder();
        for (String word : words) {
            stringBuilder.append(word.substring(0, 1).toUpperCase()).append(word.substring(1));
        }
        return stringBuilder.toString();
    }

    private String toSingular(String word) {
        if (!StringUtils.hasText(word)) {
            return word;
        }

        if (word.matches(".*[^aeiou]ies$")) {
            return word.replaceAll("ies$", "y");
        } else if (word.matches(".*(l|f)ves$")) {
            return word.replaceAll("ves$", "f");
        } else if (word.matches(".*(ch|sh|x|s|z)es$")) {
            return word.replaceAll("es$", "");
        } else if (word.matches(".*[a-z]s$") && !word.endsWith("ss")) {
            return word.replaceAll("s$", "");
        } else {
            return word;
        }
    }

}
